Jerry's Log

Spring Data JPA - projection

contents

성능 최적화 기법Spring Data JPA Projection에 대해 상세하게 번역해 드리겠습니다.

기본적으로 findAll()을 사용하면, 하이버네이트는 테이블의 모든 컬럼을 조회하여 거대한 엔티티 객체에 매핑합니다.

Spring Data JPA Projection을 사용하면 엔티티 관리라는 무거운 오버헤드 없이 원하는 특정 필드만 쏙 뽑아와서 커스텀 모델에 담을 수 있습니다.

이걸 유형별로 알아보겠습니다.


1. 세 가지 프로젝션 유형

Spring Data JPA에서 프로젝션을 구현하는 방법은 크게 인터페이스 기반, 클래스 기반(DTO), 동적 프로젝션 세 가지가 있습니다.

유형 A: 인터페이스 기반 프로젝션 (가장 흔함)

가장 쉬운 방법입니다. 가져오고 싶은 필드명과 일치하는 Getter 메서드를 가진 인터페이스를 정의하면 됩니다.

1. 인터페이스 정의:

public interface UserSummary {
    // 스프링이 딱 이 두 필드만 조회합니다 (닫힌 프로젝션)
    String getName();
    String getEmail();
    
    // 오픈 프로젝션 (고급): 
    // SpEL을 사용하여 런타임에 값을 계산해서 가져올 수 있습니다.
    @Value("#{target.name + ' (' + target.email + ')'}")
    String getFullName(); 
}

2. 리포지토리에서 사용:

public interface UserRepository extends JpaRepository<User, Long> {
    // 반환 타입으로 엔티티 대신 인터페이스를 적습니다!
    List<UserSummary> findByDepartment(String department);
}

유형 B: 클래스 기반 프로젝션 (DTO)

인터페이스 대신 구체적인 자바 클래스(DTO)를 사용합니다. 객체 내부에 로직을 넣거나 불변성(Immutability)을 보장하고 싶을 때 좋습니다.

1. DTO 정의 (Java 14+라면 Record가 최고입니다):

public record UserDto(String name, String email) {
    // 주의: 파라미터 이름이 엔티티의 필드명과 정확히 일치해야 합니다!
}

일반 Class를 쓴다면 필드와 일치하는 생성자가 반드시 있어야 합니다.

2. 리포지토리에서 사용:

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserDto> findByDepartment(String department);
}

유형 C: 동적 프로젝션 (Dynamic Projections)

하나의 리포지토리 메서드로 상황에 따라 어떨 때는 Entity를, 어떨 때는 UserSummary를, 어떨 때는 UserDto를 반환받고 싶다면 어떻게 할까요?

제네릭(Generics) 사용:

public interface UserRepository extends JpaRepository<User, Long> {
    // 제네릭 <T>를 사용하여 호출하는 시점에 타입을 정합니다
    <T> List<T> findByDepartment(String department, Class<T> type);
}

사용 예시:

// 전체 엔티티 조회 (무거움)
List<User> entities = repo.findByDepartment("IT", User.class);

// 요약 정보만 조회 (가벼움)
List<UserSummary> summaries = repo.findByDepartment("IT", UserSummary.class);

2. 중첩 프로젝션 (Nested Projections - 주의할 점)

프로젝션은 연관 관계(Join)가 있는 데이터에도 사용할 수 있습니다.

엔티티 구조: User $\rightarrow$ Department

프로젝션 인터페이스:

public interface UserWithDept {
    String getName();
    
    // 중첩 프로젝션!
    DepartmentSummary getDepartment();

    interface DepartmentSummary {
        String getName();
        String getLocation();
    }
}

3. 왜 프로젝션을 써야 할까요? (이유)

  1. 성능 (SQL Select 절):
  1. 메모리 사용량 (Heap):
  1. 보안 (데이터 노출 방지):

4. 비교표

특징 엔티티 조회 (Entity) 인터페이스 프로젝션 클래스(DTO) 프로젝션
반환 타입 List<User> List<UserSummary> List<UserDto>
SQL SELECT * (모든 컬럼) SELECT 특정_컬럼 SELECT 특정_컬럼
JPA 상태 Managed (관리됨/추적됨) Read-only (읽기 전용) Read-only (읽기 전용)
불변성 변경 가능 (Mutable) 읽기 전용 읽기 전용
프록시 오버헤드 낮음 높음 (스프링이 프록시 생성) 낮음 (생성자 직접 호출)
최적 사용처 데이터 수정(CUD) 조회 전용 API / 목록 고성능 조회 API

개발자를 위한 요약

조회 중심(Read-Heavy) API (대시보드, 목록 보기, 검색 결과 등)를 만들 때는 프로젝션을 사용하세요.

references